Projet Zuul de conception orientée objet en Java d'un jeu d'aventure
Forum des exercices du projet Zuul
Exercice 7.47
ln the processCommand method in Game, there is a sequence of if statements to dispatch commands when a command word is recognized.
This is not a very nice design, since every time we add a command, we have to add a case here in this if statement. Can you improve this design?
Design the classes so that handling of commands is more modular, and
new commands can be added more easily. Implement it. Test it.
S'inspirer de la conception montrée dans le projet zuul-even-better ci-joint et dont on peut lire les documentations utilisateur et programmeur.
Attention ! Bien lire la discussion ci-dessous
(notamment la réponse du 19 novembre 2013) pour éviter toute régression.
Tout recompiler (Tools/Rebuild Package) : il ne doit subsister aucun avertissement.
Ensuite, peu de modifications structurelles devraient avoir lieu ; il est donc possible de commencer à introduire dans son jeu tous les éléments du scénario complet (lieux, items, images, suppléments, ...).
Partie optionnelle :
Cet exercice peut également vous inspirer si vous souhaitez reconcevoir
la gestion de différents types de pièces, d'items, de personnages, ...
Un étudiant a écrit :
Bonjour,
Dans le cadre de l'exercice 7.47 visant à intégrer la conception de zuul-even-better, je me heurte à un problème : je ne vois pas comment combiner l'abstract Command avec les enums CommandWords.
Pour moi, si chaque commande a sa classe, l'enum ne sert plus.
Ai-je raison ? Si oui, supprime-t-on la conception des enums ? Si non, pourquoi, et comment combiner les 2 ?
Cordialement
Excellente question !
D'abord d'un point de vue général, il est bon de poser ce genre de question plutôt que de faire n'importe quoi; vous avez également pu constater que souvent dans les exercices précédents, une nouvelle version de Zuul vous était donnée comme exemple de mise en oeuvre d'un point particulier, mais qu'elle ne reprenait pas toutes les améliorations qui avaient été apportées à d'autres aspects du jeu.
Venons-en maintenant à l'exercice 7.47. Tout d'abord, c'est probablement un des exercices les plus difficiles de ce projet. C'est également celui qui demande le plus de modifications dans le code déjà écrit. J'espère qu'en lisant le chapitre 7 vous en comprendrez tout l'intérêt.
Alors, doit-on supprimer le long switch sur les commandes pour le remplacer par un simple appel de méthode (judicieusement redéfinie différemment pour chaque commande) ? OUI
Donc, doit-on supprimer les enum ? NON
Chaque String tapée est d'abord transformée en un CommandWord (enum), et une HashMap permet de retrouver le second à partir de la première. Ensuite, à chaque CommandWord est associée une Command qui contient la fameuse méthode à exécuter, et une HashMap permet de retrouver la seconde à partir du premier.
Cette conception permet donc de séparer la mécanique du jeu (CommandWord -> Command) de son interface textuelle (String -> CommandWord) en utilisant une seule instruction : "exécute la méthode associée à la Command associée au CommandWord associé à la String tapée".
Bien entendu, l'interface graphique peut court-circuiter une étape si elle fournit directement le CommandWord plutôt que la String.
Bon travail.
Attention ! zuul-even-better n'utilise pas de for-each mais des itérateurs ; il n'y a aucune raison de modifier votre code si vous avez utilisé des for-each à chaque fois que c'était possible.
Bonjour,
je suis actuellement bloqué dans cet exercice car dans la classe Parser, la méthode getCommand() doit renvoyer une commande, or la classe Command étant abstraite, on ne peut donc plus l'instancier...
J'ai pensé à récupérer séparément le CommandWord et le String composant une commande, et de les entrer en paramètre pour les méthodes demandant normalement un paramètre de type Command. Mais je ne pense pas que cela soit correct.
A part cela je ne vois vraiment pas comment faire contourner ce problème.
Pourriez-vous me donner une indication?
Cordialement.
Il faut d'abord bien relire ma réponse à la 1ère question ci-dessus pour comprendre que depuis les exercices sur les enum, nous disposons de 2 HashMap : une String -> CommandWord et une CommandWord -> Command.
Cela nous permet donc de récupérer très facilement l'objet Command à partir de la String tapée.
(objet Command ne veut évidemment pas dire que l'on instancie la classe Command puisqu'elle est abstraite, mais qu'il s'agit d'un objet d'une sous-classe de Command)
Bonsoir.
Suite à la lecture de l'exercice et des réponses données, deux problèmes se posent pour moi.
Si je veux conserver l'enum (qui fait double-emploi avec les sous-classes d'abstract command, ce qui cause toute la difficulté de cet exercice), je dois passer par une forme de Command générale, ce qui se faisait jusqu'à cet exercice.
Poblème (déjà pointé dans le message précedent), on ne peut plus instancier de command. Utiliser un command word : ok, mais que fait-on du SecondWord ? S'il s'agit de passer de string à {commandword + secondword} à command, le tout dans la même méthode, je commence à sérieusement douter de l'utilité de l'opération, je pense donc que cette solution n'est pas celle attendue.
Ma question principale est donc : faut-il faire le double passage string -> CW + SW -> command dans la même méthode ? Sinon, passer CW et SW simultanément d'une méthode à l'autre nécessite une classe contenant ses deux élements, mais là on revient à la solution du Command pas abstract, utilisé jusqu'ici ...
que faire ?
> Si je veux conserver l'enum (qui fait double-emploi avec les sous-classes d'abstract command, ce qui cause toute la difficulté de cet exercice),
>
??? Je ne vois pas en quoi cela fait double emploi. Peut-être voulez-vous parler du switch plutôt que des enum ?
Dans ce cas, relisez ma réponse à la première question posée sur cet exercice.
> Poblème (déjà pointé dans le message précedent), on ne peut plus instancier de command.
>
Pourquoi est-ce gênant de ne pas pouvoir instancier la classe Command ? Ce que nous voulons, c'est créer de vraies commandes qui ont chacune leur rôle, telle que GoCommand ou HelpCommand. Ces commandes doivent être créées au fur et à mesure de l'écriture de la 2èmeenum.
> Utiliser un command word : ok, mais que fait-on du
SecondWord ? S'il s'agit de passer de string à {commandword +
secondword} à command, le tout dans la même méthode, je commence à
sérieusement douter de l'utilité de l'opération, je pense donc que cette
solution n'est pas celle attendue.
>
Ne confondez pas la String aCommandWord du début avec l'enum CommandWord de maintenant !
Pour savoir comment traiter le second mot, regardez le Parser et la méthode setSecondWord de Command ...
Le compilateur vous dit qu'il ne trouve pas la variable GO, sans s'apercevoir qu'il s'agit d'une constante énumérée ...
Regardez dans le constructeur de la classe CommandWords du projet zuul-with-enums-v1 quelle notation employer pour que votre instruction ne génère plus d'erreur.
Ensuite, regardez le constructeur de la classe CommandWords du projet zuul-with-enums-v2
pour voir comment faire une boucle qui traite toutes les commandes sans
les nommer une à une, ce qui vous évitera de chercher à désigner la
commande GO en particulier.
Un étudiant a écrit :
Pour la méthode execute de chaque commande j'ai choisi de mettre en paramètre un Command et un GameEngine, est-ce une conception possible ou faut-il que je change?
Il faudrait voir comment vous avez écrit tout le reste pour donner un avis plus définitif, mais a priori, je ne vois pas pourquoi passer un paramètre Command à une méthode d'une sous-classe de Command ...
Pour GameEngine, pourquoi pas ? Il faut voir si le paramètre Player ne suffit pas, selon la façon dont vous avez prévu de récupérer la référence vers la UserInterface.
Bonsoir,
J'ai un problème que je n'arrive pas à résoudre. J'ai changé ma classe Command en classe Abstraite et j'ai créé différentes sous-classes( pour l'instant 3 comme dans le zuul-even-better). J'ai fait tous les changements dans différentes classes.
Lorsque je lance mon jeu, et que je tape aide, quitter ou go sans second mot, les commandes sont exécutées. Mais lorsque je tape go avec un second mot par exemple Nord, il ne reconnait pas la commande. Donc je voudrais savoir s'il faut faire encore une sous-classe de GoCommand pour les différentes directions, ou est-ce possible d'extraire la commande de la string tapée pour que le jeu reconnaisse la commande et ensuite d'extraire le second mot et l'appliquer à la méthode qui dirige le joueur ?
Il n'y a pas besoin de créer de sous-classe de GoCommand puisque vous voyez qu'elle gère bien le 'secondWord'.
N'auriez-vous pas repris par erreur l'ancien Parser utilisant un Scanner au lieu de conserver celui utilisant la String provenant de l'entryField ?
L'étudiant a répondu :
Vous aviez raison , mon Passer n'étais pas bon et il me manquais une Hashmap<CommandWord , Command>.
Maintenant que tout est réglé. Un autre problème survient , c'est
l'affichage. Par exemple dans ma sous-classe HelpCommand je dois faire
afficher plusieurs String. Dans le code donné, il y a plusieurs S.o.p
mais je ne veux pas les faire afficher dans le terminal mais seulement
dans le panel. Je me suis dit que je pourrais créer un attribut gui de
type UserInterface et l'initialiser comme dans le GameEngine. Est-ce une
bonne façon de faire ? Ou est-ce qu'il y aurais une autre façon ? Si
oui pourriez-vous me donner quelques indications.
Merci
Je ne vois pas d'autre moyen effectivement que de mémoriser une référence vers la GUI.
Par contre, il y a de nombreuses possibilités pour le faire, dans Command ou dans certaines sous-classes, en static ou à la création de l'instance, à chaque exécution, ...
Choisissez la solution qui vous semble la plus raisonnable.
Bonjour,
J'ai créé toutes les nouvelles classes relatives à chaque commande mais certaines de ces classes ont besoin de méthodes présentes dans
GameEngine (comme par exemple la méthode interpretCommand pour la Commande Test).
Je ne vois vraiment pas comment faire pour avoir accès à ces méthodes.
Merci.
Monsieur, je comprends ce que vous voulez faire mais je ne vois pas comment le faire.
- Lorsque je crée les commandes je suis dans l'enum CommandWord où à un CommandWord j'ai associé le command word et la Command mais je n'ai pas accès au GameEngine à moins que je me sois trompé et que la création des commandes ne se faisait pas ici.
- Dans l'autre possibilité, on a effectivement accès au Player qui est passé en paramètre mais je ne vois pas comment récupérer le GameEngine à partir du Player ( il n'y à rien dans le Player qui nous le permet).
Merci
Bonsoir, j'ai plusieurs questions par rapport à cette exercice.
Tout d'abord je ne comprends pas vraiment l'intérêt de créer deux HashMap s plutôt qu'une seule comme illustré dans Zuul-even-better. De plus je ne vois pas vraiment où mettre la deuxième si ce n'est dans CommandWords avec la première. Est-ce juste dans un soucis de "bonne programmation" ?
Ensuite comme vous l'avez suggéré je préfère envoyer GameEngine en paramètre plutôt que Player pour execute(). Cependant je me retrouve à utiliser beaucoup de getters à l'intérieur de mes fonctions... Est-ce que c'est un problème ? Je me pose la question car pour l'implémentation de Player cela m'embêtais déjà et j'ai essayé de réduire leur nombre au maximum.
Enfin j'avais l'habitude dans les méthodes GoRoom et Back d'appeller Look pour afficher les lieux lorsqu'on se déplace d'une pièce à une autre (idem pour les téléportations etc). Or maintenant que les méthodes associées aux commandes sont disjointes il faudrait créer un objet LookCommand et l'éxecuter pour afficher les lieux. Est-ce que c'est envisageable ou est-ce qu'il y a une meilleur manière pour éviter la duplication de code ?
- La HashMap te sert a retrouver la classe Command correspondant à la commande que l'utilisateur à tapé.
- Je pense que utiliser la méthode printLocationInfo qui se trouve GameEngine serait plus simple.
- après j'ai aussi plein de getters que j'utilise dans chaque fonction mais je pense qu'on est obligé.
Voila j'espère que sa t'as aidé
Pour compléter la réponse ci-dessus, la 2ème HashMap sert à obtenir l'indépendance entre la String
que l'on tape et la commande qui est déclenchée. Il n'y aura qu'un seul
endroit à modifier si on décide de remplacer la commande "aller" par
"va", par exemple.
Par ailleurs, pour diminuer les appels de getters, ne pouvez-vous pas stocker certaines références dans des variables locales ?
Un étudiant a écrit :
Bonjour,je vous envoie ce message pour avoir une petite précision sur abstract Command.
En fait, je me demandais si je devais supprimer toutes les méthodes ayant en paramètre un objet Command de la classe Player et GameEngine et ensuite de les réécrire dans leur classe command respective qui hérite d'abstract Command?
par exemple pour la méthode go:
-> au lieu de faire: *dans la classe GoCommand:
public boolean execute (final Player pPlayer) { pPlayer.walk(this); }
*et dans la classe Player avoir la méthode:
public void walk(final Command pCmd) { //code }-> j'aurais juste: *dans GoCommand:
public boolean execute (final Player pPlayer) { //code de la fonction walk qui était dans la classe Player }*et dans la classe Player:
je n'aurais plus cette méthode
Merci pour votre attention,
Cordialemen,
La réponse est un peu un mélange de vos 2 solutions :
- dans GoCommand, on doit gérer la commande : que faire s'il y a ou pas un second mot, et par exemple, demander au Player de changer de pièce
- dans Player, on doit gérer une action du joueur : par exemple, changer de pièce
Inspirez-vous du projet zuul-even-better et des réponses aux questions déjà posées sur cet exercice.
Bonjour a vous,
Est
ce que la méthode execute doit absolument renvoyer un booléen ? Dans
zuul-even-better c'est assez simple pour eux comme ils utilisent
System.println() dans leurs classes **Command..
Et en essayant de faire l'exercice, j'ai, comme il est dit plus haut, beaucoup d'accesseurs.. Seulement est ce correct d'avoir également des accesseurs des UserInterface de Player et GameEngine ?
Merci d'avance de vos réponses
1) Le booléen sert normalement à indiquer la fin du jeu. On peut sans doute faire autrement et s'en passer, pour pouvoir retourner une String qui serait finalement affichée dans le GameEngine, mais n'aura-t-on jamais à afficher de message intermédiaire au cours de l'exécution d'une commande ?
2) Dans certaines commandes, l'accès à UserInterface (et probablement à GameEngine) sera indispensable. Puisqu'on dispose du Player en paramètre d'execute, on pourrait imaginer des accesseurs dans Player (pour faciliter l'écriture, on peut stocker les références dans des variables locales au début d'execute). On pourrait aussi les stocker dans Command pour éviter d'avoir à refaire la même chose à chaque appel d'execute.
Un étudiant a écrit :
Un étudiant a écrit :
Réseaux sociaux